using System.Data.Common;
using System.Diagnostics;
using LinqToDB;
using LinqToDB.Common;
using LinqToDB.Tools;
using NalogGarLoader.Data;
using NalogGarLoader.Data.Tables;
using NalogGarLoader.Model;
using NalogGarLoader.Model.GarRepositoryReader;
using NalogGarLoader.Model.NalogServices;

namespace NalogGarLoader;

public class GarImportController
{
    private static readonly NLog.Logger log = NLog.LogManager.GetCurrentClassLogger();
    public GarImportParameters GarImportParameters { protected get; init; }
    private HashSet<string> TableImportSet { get; init; }

    public GarImportController(GarImportParameters garImportParameters)
    {
        ArgumentNullException.ThrowIfNull(garImportParameters);
        garImportParameters.Validate();

        this.GarImportParameters = garImportParameters;
        this.TableImportSet = this.GarImportParameters.IncludeGarTypes ?? GarTypes.GarTypeSet.ToHashSet();

        if (this.GarImportParameters.ExcludeGarTypes is not null)
            this.TableImportSet.ExceptWith(this.GarImportParameters.ExcludeGarTypes);

        GarDbContext.DataOptions = new DataOptions()
            .UseConnectionString(this.GarImportParameters.ProviderName!, this.GarImportParameters.ConnectionString!)
            .UseDefaultRetryPolicyFactory()
            ;
    }

    public void RunImport()
    {
        this.InitDb();

        if (this.GarImportParameters.InitDbOnly)
            return;

        List<string> importList = this.InitImportList();

        foreach (var importSource in importList)
        {
            Stopwatch importSourceStopWatch = Stopwatch.StartNew();

            string? localFileName;

            switch (this.GarImportParameters.ImportMode)
            {
                case ImportMode.Full:
                case ImportMode.Update:
                    localFileName = DownloadFile(importSource);
                    break;
                default:
                    localFileName = importSource;
                    break;
            }

            log.Info($"Начало импорта из источника: {localFileName}");

            GarRepositoryReader garRepositoryReader = GarRepositoryReader.Open(localFileName, this.TableImportSet);

            log.Info($"Версия ГАР {garRepositoryReader.GarVersion}");

            ParallelOptions ParallelOptions = new()
            {
                MaxDegreeOfParallelism = this.GarImportParameters.MaxDOP
            };

            Parallel.ForEach(garRepositoryReader, ParallelOptions, this.ProcessRepositoryEntry);

            SaveImportLog(garRepositoryReader.GarVersion, garRepositoryReader.IsIncremental);

            importSourceStopWatch.Stop();

            log.Info($"{localFileName} импортирован за {importSourceStopWatch.Elapsed.ToString("hh\\:mm\\:ss")}");

            if (this.GarImportParameters.ImportMode != ImportMode.Manual)
            {
                DeleteFile(localFileName);
            }
        }
    }

    private string DownloadFile(string fileUri)
    {
        
        string saveFileName = Path.Combine(this.GarImportParameters.TmpDir, $"{Path.GetFileName(fileUri)}");

        FileDownloader fileDownloader = new FileDownloader();
        fileDownloader.DownloadFile(fileUri, saveFileName);
        return saveFileName;
    }

    private void ProcessRepositoryEntry(GarRepositoryEntry repositoryEntry)
    {
        if (!string.IsNullOrEmpty(repositoryEntry.StateId))
        {
            if (this.GarImportParameters.IncludeStates is not null && !this.GarImportParameters.IncludeStates.Contains(repositoryEntry.StateId))
                return;

            if (this.GarImportParameters.ExcludeStates?.Contains(repositoryEntry.StateId) == true)
                return;
        }

        Stopwatch fileImportStropWatch = Stopwatch.StartNew();
        uint imported = 0;

        using (GarFileImporter garFileImporter = GarFileImporter.Create(repositoryEntry, this.GarImportParameters))
        {
            imported = garFileImporter.Import();
        }

        fileImportStropWatch.Stop();

        log.Info($"Файл {(!string.IsNullOrEmpty(repositoryEntry.StateId) ? $"{repositoryEntry.StateId}\\" : "")}{repositoryEntry.FileName} импортирован. {imported} записей за {fileImportStropWatch.Elapsed.ToString("hh\\:mm\\:ss")}");

        GC.Collect();
    }

    private static void SaveImportLog(DateOnly garVersion, bool? isIncremental)
    {
        using var db = new GarDbContext();
        
        ImportLog importLog = new()
        {
            GarVersion = garVersion,
            IsIncremental = isIncremental
        };

        db.Insert(importLog);

        CurrentVersion currentVersion = new()
        {
            GarVersion = garVersion,
            LastImportDateTime = importLog.ImportDateTime
        };

        db.InsertOrReplace(currentVersion);

        log.Trace($"Сохранена информация об импорте в БД: GarVersion = {importLog.GarVersion}, IsIncremental = {importLog.IsIncremental}");
    }

    private static void DeleteFile(string fileName)
    {
        int retryCounter = 0;

        while (File.Exists(fileName))
        {
            try
            {
                log.Trace($"Удаление файла {fileName} ({retryCounter})");

                File.Delete(fileName);

                log.Trace($"Файл удален {fileName}");
            }
            catch
            {
                retryCounter++;

                switch (retryCounter)
                {
                    case <= 10:
                        Thread.Sleep(1000 * retryCounter);
                        break;
                    default:
                        throw;
                }
            }
        }
    }

    private List<string> InitImportList()
    {
        if (this.GarImportParameters.ImportMode == ImportMode.Manual)
        {
            log.Info($"Выбран ручной режим. Импорт из: {this.GarImportParameters.Source}");
            return [this.GarImportParameters.Source!];
        }

        GarRequestFileListHelper garRequestFileListHelper = new();

        if (this.GarImportParameters.ImportMode == ImportMode.Full)
        {
            return [ this.GarImportParameters.LastImportGarVer.HasValue
                ? garRequestFileListHelper.GetFullOnDate(this.GarImportParameters.LastImportGarVer.Value)
                : garRequestFileListHelper.GetLatestFull()];
        }

        if (this.GarImportParameters.ImportMode == ImportMode.Update)
        {
            DateOnly? lastImportGarVer = this.GarImportParameters.LastImportGarVer;

            if (lastImportGarVer is null)
            {
                using var db = new GarDbContext();

                CurrentVersion? currentVersion = db.CurrentVersion.FirstOrDefault();
                ImportLog? lastImportLog = lastImportLog = db.ImportLog.OrderByDescending(l => l.Id).FirstOrDefault();

                lastImportGarVer = currentVersion?.GarVersion ?? lastImportLog?.GarVersion;
            }

            if (lastImportGarVer is null)
                throw new ImportLogRecordNotFoundException("Не удалось определить последнюю импорированную версию ГАР. Обновление невозможно.");

            var updateList = garRequestFileListHelper.GetUpdates(lastImportGarVer.Value);

            if (updateList.IsNullOrEmpty())
                log.Info("Обновления отсутствуют");

            return updateList;
        }

        throw new ArgumentException("Указаны некорректные параметры: не удалось инициализировать список для импорта");
    }

    private void InitDb()
    {
        try
        {
            using GarDbInitializator dbInitializator = new(this.TableImportSet, this.GarImportParameters.CleanupMode);
            dbInitializator.RunInit();
        }
        catch (Exception ex)
        {
            log.Fatal(ex, "Исключение при инициализации БД");
            throw;
        }
    }
}